#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#define __USE_XOPEN
#include <time.h>
#include <glib.h>
#include <glib/gstdio.h>

#ifdef USE_ONIGURUMA
# include <oniguruma.h>
#endif

#ifdef USE_ONIGURUMA
typedef OnigRegexType Regex;
typedef OnigRegion RegexData;
#else
typedef GRegex Regex;
typedef GMatchInfo RegexData;
#endif

Regex *regex_new(const gchar *pat, gboolean multi_line) {
#ifdef USE_ONIGURUMA
  OnigRegexType *re = NULL;
  OnigOptionType ot = ONIG_OPTION_DEFAULT;
  if (multi_line) ot |= ONIG_OPTION_NEGATE_SINGLELINE;
  onig_new(&re, (const UChar *)pat, (const UChar *)(pat + strlen(pat)),
           ot, ONIG_ENCODING_ASCII, ONIG_SYNTAX_PERL, NULL);
  return re;
#else
  GRegexCompileFlags cf = G_REGEX_OPTIMIZE;
  if (multi_line) cf |= G_REGEX_MULTILINE;
  return g_regex_new(pat, cf, 0, NULL);
#endif
}

gboolean regex_match(Regex *re, const gchar *str, RegexData **data) {
#ifdef USE_ONIGURUMA
  OnigRegion *reg = onig_region_new();
  const UChar *s = (UChar *)str, *e = (UChar *)(str + strlen(str));
  if (onig_match(re, s, e, s, reg, ONIG_OPTION_NONE) >= 0) {
    *data = reg;
    return TRUE;
  }
  onig_region_free(reg, 1);
  return FALSE;
#else
  GMatchInfo *m = NULL;
  if (g_regex_match(re, str, 0, &m)) {
    *data = m;
    return TRUE;
  }
  if (m) g_match_info_free(m);
  return FALSE;
#endif
}
gchar *regex_data_fetch(RegexData *data, gint i, const gchar *start) {
#ifdef USE_ONIGURUMA
  return (data->num_regs > i
          ? g_strndup(start + data->beg[i], data->end[i] - data->beg[i])
          : NULL);
#else
  return g_match_info_fetch(data, i);
#endif
}
void regex_data_free(RegexData *data) {
#ifdef USE_ONIGURUMA
  onig_region_free(data, 1);
#else
  g_match_info_free(data);
#endif
}

typedef struct {
  gchar *yp;
  time_t time;
  gchar *time_s;
  guint  count;
} SourceEntry;

gint se_cmp(SourceEntry *a, SourceEntry *b) {
  return a->time - b->time;
}

gboolean add_log_line(GHashTable *source,
                      GList     **keys,
                      gint       *year,
                      gint       *lm,
                      gchar     **m,
                      time_t      range_start,
                      time_t      range_end) {
  struct tm *t = (struct tm *)g_malloc0(sizeof(struct tm));
  gint month = atoi(m[1]) - 1;
  if (month < *lm) ++*year;
  *lm = month;
  t->tm_year = *year;
  t->tm_mon  = month;
  t->tm_mday = atoi(m[2]);
  t->tm_hour = atoi(m[3]);
  t->tm_min  = atoi(m[4]);
  t->tm_sec  = atoi(m[5]);
  time_t time = mktime(t);
  g_free(t);
  if (time > range_end) return FALSE;
  if (time >= range_start) {
    SourceEntry *se = (SourceEntry *)g_malloc(sizeof(SourceEntry));
    se->yp   = g_strdup(m[6]);
    se->time = time;
    se->time_s = g_strdup_printf("%d-%s-%s-%s-%s-%s",
                                 *year + 1900, m[1], m[2],
                                 m[3], m[4], m[5]);
    se->count = atoi(m[7]);

    GList **l = g_hash_table_lookup(source, se->yp);
    if (l == NULL) {
      gchar *key = g_strdup(se->yp);
      l = g_malloc0(sizeof(GList *));
      g_hash_table_insert(source, key, l);
      *keys = g_list_append(*keys, key);
    }
    *l = g_list_insert_sorted(*l, se, (GCompareFunc)se_cmp);
  }
  return TRUE;
}
void read_log(GHashTable  *source,
              GList      **keys,
              const gchar *path,
              struct tm   *created_time,
              time_t       range_start,
              time_t       range_end) {
  gchar *contents = NULL;
  gsize  length   = 0;
  if (g_file_get_contents(path, &contents, &length, NULL)) {
    static Regex *re = NULL;
    if (!re) re = regex_new("^(\\d+)/(\\d+) (\\d+):(\\d+):(\\d+) "
                            "[LED]: Complete: (.+?) (\\d+)", TRUE);
#ifdef USE_ONIGURUMA
    OnigRegion *region = onig_region_new();
#else
    GMatchInfo *match = NULL;
#endif
    gint year = created_time->tm_year;
    gint lm = 0;
#ifdef USE_ONIGURUMA
    const gchar *str_s = contents;
    const gchar *str_e = contents + length;
    while(onig_search(re,
                      (const UChar *)str_s, (const UChar *)str_e,
                      (const UChar *)str_s, (const UChar *)str_e, region,
                      ONIG_OPTION_NONE) >= 0) {
      gchar **m = g_malloc(sizeof(char *) * 9);
      m[0] = regex_data_fetch(region, 0, str_s);
      m[1] = regex_data_fetch(region, 1, str_s);
      m[2] = regex_data_fetch(region, 2, str_s);
      m[3] = regex_data_fetch(region, 3, str_s);
      m[4] = regex_data_fetch(region, 4, str_s);
      m[5] = regex_data_fetch(region, 5, str_s);
      m[6] = regex_data_fetch(region, 6, str_s);
      m[7] = regex_data_fetch(region, 7, str_s);
      m[8] = NULL;
      gboolean res = add_log_line(source, keys, &year, &lm, m,
                                  range_start, range_end);
      g_strfreev(m);
      if (!res) break;
      str_s += region->end[0];
    }
    onig_region_free(region, 1);
#else
    g_regex_match(re, contents, 0, &match);
    gchar **m = NULL;
    while(match && (m = g_match_info_fetch_all(match))) {
      gboolean res = add_log_line(source, keys, &year, &lm, m,
                                  range_start, range_end);
      g_strfreev(m);
      if (!res || !g_match_info_next(match, NULL)) break;
    }
#endif
  }
  if (contents) g_free(contents);
}

void output(gchar      **argv,
            GHashTable  *source,
            GList       *keys,
            const gchar *datadir,
            const gchar *all_name,
            const gchar *filename,
            const gchar *title,
            const gchar *add_src,
            time_t       start,
            time_t       end) {
  GPid pid = 0;
  gint c_stdin = 0;
  g_spawn_async_with_pipes(NULL,
                           argv,
                           NULL,
                           0,
                           0,
                           0,
                           &pid,
                           &c_stdin,
                           NULL,
                           NULL,
                           NULL);

  struct tm t_s;
  struct tm t_e;
  localtime_r(&start, &t_s);
  localtime_r(&end,   &t_e);

  FILE *f_stdin = fdopen(c_stdin, "w");
  fprintf(f_stdin, "set title \"%s\"\n", title);
  fprintf(f_stdin, "set pal maxcolors 0\n");
  fprintf(f_stdin, "set key outside\n");
  fprintf(f_stdin, "set grid xtics mxtics ytics front\n");
  fprintf(f_stdin, "set output \"%s\"\n", filename);
  fprintf(f_stdin, "set ylabel \"Channels\"\n");
  fprintf(f_stdin, "set xdata time\n");
  //fprintf(f_stdin, "set timefmt \"%s\"\n");
  fprintf(f_stdin, "set timefmt \"%%Y-%%m-%%d-%%H-%%M-%%S\"\n");
  //fprintf(f_stdin, "set xrange [\"%d\":\"%d\"]\n", start, end);
  fprintf(f_stdin,
          "set xrange [\"%d-%d-%d-%d-%d-%d\":\"%d-%d-%d-%d-%d-%d\"]\n",
          t_s.tm_year + 1900, t_s.tm_mon + 1, t_s.tm_mday,
          t_s.tm_hour, t_s.tm_min, t_s.tm_sec,
          t_e.tm_year + 1900, t_e.tm_mon + 1, t_e.tm_mday,
          t_e.tm_hour, t_e.tm_min, t_e.tm_sec);
  fprintf(f_stdin, "set x2range [%ld:%ld]\n", start, end);
  fprintf(f_stdin, "set y2range [0:1]\n");
  fprintf(f_stdin, "set style data lines\n");

  time_t dur = end - start;
  const char *terminal = "set terminal png truecolor size %i,%i\n";
  //const char *terminal = "set terminal png truecolor enhanced font \"cour\" 10 size %i,%i\n";
  if (dur <= 86400) {
    // 1d
    fprintf(f_stdin, terminal, 720, 480);
    fprintf(f_stdin, "set xtics 43200\n");
    fprintf(f_stdin, "set mxtics 6\n");
    fprintf(f_stdin, "set format x \"%%m/%%d\\n%%a\\n%%H:%%M\"\n");
  } else {
    fprintf(f_stdin, "set xtics 86400\n");
    if (dur <= 259200) {
      // 3d
      fprintf(f_stdin, terminal, 720, 480);
      fprintf(f_stdin, "set mxtics 4\n");
    } else if (dur <= 1209600) {
      // 2w
      fprintf(f_stdin, terminal, 1280, 480);
      fprintf(f_stdin, "set mxtics 2\n");
    } else {
      fprintf(f_stdin, terminal, 2480, 480);
      fprintf(f_stdin, "set mxtics 2\n");
    }
    fprintf(f_stdin, "set format x \"%%m/%%d\\n%%a\"\n");
  }

  fprintf(f_stdin, "\n");
  if (add_src) {
    FILE  *as = fopen(add_src, "rb");
    char   buf[64];
    size_t size = 0;
    while((size = fread(buf, 1, sizeof(buf), as))) {
      while((size -= fwrite(buf, 1, size, f_stdin))) ;
    }
    fclose(as);
  }
  fprintf(f_stdin, "\n");

  gchar *all = g_build_filename(datadir, all_name, NULL);
  fprintf(f_stdin, "plot \"%s\" using 1:2 title \"All\" with filledcurves y1=0 lc rgb \"#eaeaea\"", all);

  GList *key;
  for(key = keys; key; key = key->next) {
    gchar *path = g_build_filename(datadir, key->data, NULL);
    fprintf(f_stdin, ", \"%s\" using 1:2 title \"%s\"", path, (char *)key->data);
    g_free(path);
  }
  fprintf(f_stdin, ", \"%s\" using 1:2 notitle with line lc \"#d0d0d0\"", all);
  fprintf(f_stdin, "\n");
  g_free(all);

  fclose(f_stdin);
  g_spawn_close_pid(pid);
}

void write_data(const gchar *filename,
                GList      **data,
                const gchar *datadir) {
  gchar *path = g_build_filename(datadir, filename, NULL);
  FILE  *f    = fopen(path, "w");
  GList *d    = *data;
  for(; d; d = d->next) {
    SourceEntry *se = (SourceEntry *)d->data;
    fprintf(f, "%s %d\n", se->time_s, se->count);
  }
  fclose(f);
  g_free(path);
}
void write_all_data(const gchar *datadir,
                    const gchar *filename,
                    GHashTable  *source,
                    GList       *keys,
                    time_t       start,
                    time_t       end) {
  gchar *path = g_build_filename(datadir, filename, NULL);
  FILE  *f    = fopen(path, "w");

  GList **dats = NULL;
  gsize   klen = 0;
  {
    klen = g_list_length(keys);
    dats = g_malloc0(sizeof(GList *) * klen);

    gsize  i;
    GList *key = keys;
    for(i = 0; i < klen; ++i) {
      dats[i] = *(GList **)g_hash_table_lookup(source, key->data);
      key = key->next;
    }
  }

  time_t t;
  gsize  lc = 0;
  for(t = start; t <= end; ++t) {
    gsize i;
    gsize c  = 0;
    const gchar *time_s = NULL;
    for(i = 0; i < klen; ++i) {
      while(dats[i]->next &&
            ((SourceEntry *)dats[i]->next->data)->time <= t) {
        dats[i] = dats[i]->next;
      }
      SourceEntry *se = dats[i]->data;
      c += se->count;
      if (se->time == t) time_s = se->time_s;
    }
    if (time_s && c != lc) {
      fprintf(f, "%s %d\n", time_s, c);
      lc = c;
    }
  }

  fclose(f);
  g_free(path);
}

typedef enum {
  OUTPUT_TYPE_STANDARD,
  OUTPUT_TYPE_AVERAGE,
} OutputType;

typedef struct {
  OutputType type;
  gchar     *filename;
  gchar     *title;
  time_t     start;
  time_t     end;
} OutputContext;

OutputContext *output_context_new(OutputType   type,
                                  const gchar *filename,
                                  const gchar *title,
                                  time_t       start,
                                  time_t       end) {
  OutputContext *oc = (OutputContext *)g_malloc(sizeof(OutputContext));
  oc->type     = type;
  oc->filename = g_strdup(filename);
  oc->title    = g_strdup(title);
  oc->start    = start;
  oc->end      = end;
  return oc;
}

time_t get_time_from_regex_data_full(RegexData   *data,
                                     const gchar *start,
                                     gboolean    *set_m,
                                     gboolean    *set_d,
                                     struct tm   *ret_tm) {
  gchar *y = regex_data_fetch(data, 1, start);
  gchar *m = regex_data_fetch(data, 2, start);
  gchar *d = regex_data_fetch(data, 3, start);

  struct tm t;
  memset(&t, 0, sizeof(t));
  t.tm_year = atoi(y) - 1900;
  t.tm_mon  = 0;
  t.tm_mday = 1;
  t.tm_hour = 6;
  if (m && *m) {
    t.tm_mon = atoi(m) - 1;
    if (d && *d) t.tm_mday = atoi(d);
  }

  if (set_m) *set_m = m != NULL && *m != '\0';
  if (set_d) *set_d = d != NULL && *d != '\0';
  if (ret_tm) memcpy(ret_tm, &t, sizeof(t));

  g_free(y);
  if (m) g_free(m);
  if (d) g_free(d);
  return mktime(&t);
}
time_t get_time_from_regex_data(RegexData *data, const gchar *start) {
  return get_time_from_regex_data_full(data, start, NULL, NULL, NULL);
}

OutputContext *create_output_context(OutputType type, const gchar *line, time_t now) {
  static Regex *re_dur  = NULL;
  static Regex *re_date = NULL;
  if (!re_dur) re_dur = regex_new("^(\\d+)([wdh])$", FALSE);
  if (!re_date) {
    re_date = regex_new("^(\\d{4})(?:/(\\d{1,2}))?(?:/(\\d{1,2}))?$", FALSE);
  }

  gchar        **l  = g_strsplit(line, ":", 0);
  OutputContext *oc = NULL;
  if (l && l[0] && l[1] && l[2]) {
    if (l[3]) {
      RegexData *d1 = NULL;
      RegexData *d2 = NULL;
      if (regex_match(re_date, l[2], &d1) &&
          regex_match(re_date, l[3], &d2)) {
        oc = output_context_new(type, l[0], l[1],
                                get_time_from_regex_data(d1, l[2]),
                                get_time_from_regex_data(d2, l[3]));
      }
      if (d1) regex_data_free(d1);
      if (d2) regex_data_free(d2);
    } else {
      RegexData *d1 = NULL;
      RegexData *d2 = NULL;
      if (regex_match(re_dur, l[2], &d1)) {
        gchar *d = regex_data_fetch(d1, 1, l[2]);
        gchar *s = regex_data_fetch(d1, 2, l[2]);
        guint  o = 0;
        switch(s[0]) {
        case 'w': o = 60 * 60 * 24 * 7 * atoi(d); break;
        case 'd': o = 60 * 60 * 24 * atoi(d);     break;
        case 'h': o = 60 * 60 * atoi(d);          break;
        }
        if (o) {
          oc = output_context_new(type, l[0], l[1], 0, 0);
          oc->start = now - o;
          oc->end   = now;
        }
        g_free(d);
        g_free(s);
      } else if (regex_match(re_date, l[2], &d2)) {
        struct tm t;
        gboolean m, d;
        time_t start = get_time_from_regex_data_full(d2, l[2], &m, &d, &t);
        time_t end;

        if (m) {
          if (d) {
            end = start + (60 * 60 * 24);
          } else {
            ++t.tm_mon;
            while(t.tm_mon > 11) {
              ++t.tm_year;
              t.tm_mon -= 12;
            }
            end = mktime(&t);
          }
        } else {
          ++t.tm_year;
          end = mktime(&t);
        }
        oc = output_context_new(type, l[0], l[1], start, end);
      }
      if (d1) regex_data_free(d1);
      if (d2) regex_data_free(d2);
    }
  }
  g_strfreev(l);
  return oc;
}

GSList *output_append_line(GSList *outputs, OutputType type, const gchar *line, time_t now) {
  if (line && *line) {
    OutputContext *oc = create_output_context(type, line, now);
    if (oc) {
      return g_slist_append(outputs, oc);
    } else {
      fprintf(stderr, "invalid output - %s\n", line);
    }
  }
  return outputs;
}
GSList *output_append_lines(GSList *outputs, OutputType type, const gchar *str, time_t now) {
  gchar **lines = g_strsplit(str, "\n", 0);
  gchar **l;
  for(l = lines; *l; ++l) {
    outputs = output_append_line(outputs, type, *l, now);
  }
  g_strfreev(lines);
  return outputs;
}

int main(int argc, char *argv[]) {
  const gchar *command  = NULL;
  const gchar *logdir   = NULL;
  const gchar *datadir  = NULL;
  const gchar *all_name = NULL;
  const gchar *add_src  = NULL;
  GHashTable  *source   = NULL;
  GList       *keys     = NULL;

  time_t  now     = time(NULL);
  GSList *outputs = NULL;

  if (argc < 2) {
    fprintf(stderr,
            "%s [-c /usr/bin/gnuplot]"
            " [-l ~/.config/gpyp3/log]"
            " [-t standard|average]"
            " [-d data]"
            " [-a __ALL__]"
            " [-e outputs]"
            " [-f additional script]"
            " [file]\n",
            argv[0]);
    exit(1);
  }

  {
    OutputType type = OUTPUT_TYPE_STANDARD;
    int i = 1;
    for(; i < argc; ++i) {
      if (strcmp(argv[i], "-c") == 0 && ++i < argc) {
        command = argv[i];
      } else if (strcmp(argv[i], "-l") == 0 && ++i < argc) {
        logdir = argv[i];
      } else if (strcmp(argv[i], "-t") == 0 && ++i < argc) {
        if (strcmp(argv[i], "standard") == 0) {
          type = OUTPUT_TYPE_STANDARD;
        } else if (strcmp(argv[i], "average") == 0) {
          type = OUTPUT_TYPE_AVERAGE;
        }
      } else if (strcmp(argv[i], "-d") == 0 && ++i < argc) {
        datadir = argv[i];
      } else if (strcmp(argv[i], "-e") == 0 && ++i < argc) {
        outputs = output_append_lines(outputs, type, argv[i], now);
      } else if (strcmp(argv[i], "-f") == 0 && ++i < argc) {
        add_src = argv[i];
      } else if (strcmp(argv[i], "-") == 0) {
        char line[256];
        while(fgets(line, sizeof(line), stdin)) {
          char *n = strchr(line, '\n');
          if (n) *n = '\0';
          outputs = output_append_line(outputs, type, line, now);
        }
      } else {
        if (g_file_test(argv[i], G_FILE_TEST_IS_REGULAR)) {
          gchar *contents = NULL;
          gsize  length   = 0;
          if (g_file_get_contents(argv[i], &contents, &length, NULL)) {
            outputs = output_append_lines(outputs, type, contents, now);
          } else {
            fprintf(stderr, "failed to read file - %s\n", argv[i]);
            exit(1);
          }
          if (contents) g_free(contents);
        } else {
          fprintf(stderr, "no such file - %s\n", argv[i]);
          exit(1);
        }
      }
    }
  }

  if (command == NULL) command = "/usr/bin/gnuplot";
  if (logdir == NULL) {
    logdir = g_build_filename(g_get_user_config_dir(), "gpyp3", "log", NULL);
  }
  if (datadir == NULL) datadir = "data";
  if (all_name == NULL) all_name = "__ALL__";

  gint    command_argc = 0;
  gchar **command_argv = NULL;
  if (!g_shell_parse_argv(command, &command_argc, &command_argv, NULL)) {
    fprintf(stderr, "failed to parse command for gnuplot\n");
    exit(1);
  }

  time_t min_start = -1;
  time_t max_end   = -1;
  {
    GSList *p;
    for(p = outputs; p; p = p->next) {
      OutputContext *oc = p->data;
      if (min_start < 0 || oc->start < min_start) min_start = oc->start;
      if (max_end   < 0 || oc->end   > max_end)   max_end   = oc->end;
    }
  }

  source = g_hash_table_new(g_str_hash, g_str_equal);
  {
    GDir  *d = g_dir_open(logdir, 0, NULL);
    const gchar *filename = NULL;
    while((filename = g_dir_read_name(d))) {
      struct tm t;
      char *e = strptime(filename, "%Y%m%d_%H%M%S.log", &t);
      if (e && *e == '\0') {
        time_t time = mktime(&t);
        if (time > max_end) continue;

        gchar *path = g_build_filename(logdir, filename, NULL);
        struct stat st;
        if (g_stat(path, &st) == 0 && st.st_mtime >= min_start) {
          read_log(source, &keys, path, &t, min_start, max_end);
        }
        g_free(path);
      }
    }
    g_dir_close(d);
    g_mkdir_with_parents(datadir, 0755);
    write_all_data(datadir, all_name, source, keys, min_start, max_end);
  }

  {
    g_hash_table_foreach(source, (GHFunc)write_data, (gpointer)datadir);

    keys = g_list_sort(keys, (GCompareFunc)strcasecmp);

    GSList *p;
    for(p = outputs; p; p = p->next) {
      OutputContext *oc = p->data;
      output(command_argv, source, keys, datadir, all_name,
             oc->filename, oc->title, add_src, oc->start, oc->end);
    }
  }
  return 0;
}
